ARMアーキテクチャ向けのDockerコンテナイメージをWindows/Macでビルドする
みなさん、こんにちは!
AWS事業本部の青柳@福岡オフィスです。
今回も、前回のブログ記事 に引き続き「Graviton2」と Docker のネタで行きたいと思います。
前回は、ARMベースプロセッサ「Graviton2」を搭載したM6gインスタンス上でDockerイメージのビルドと起動を試しました。
今回は、WindowsやMacの「Docker Desktop」を使ってDockerイメージをビルドしたいと思います。
しかし、WindowsやMacのCPUアーキテクチャは「x86」であり、ARMアーキテクチャとは互換性がありません。
そこで、どのようにすればWinodwsやMac上でARMアーキテクチャ向けのDockerイメージをビルドして「Graviton2」インスタンス上で起動することができるのか、順を追って試してみましょう。
構成および前提条件
WindowsやMacでビルドしたDockerイメージを「Graviton2」インスタンスへ渡すために、Amazon ECR のリポジトリを利用することにします。
なお、WindowsやMacの「Docker Desktop」を利用できない場合は、AWS上にAmazon Linux 2 (x86アーキテクチャ) のEC2インスタンスを起動してDockerをインストールした環境を使用しても構いません。
(これらは「x86アーキテクチャ上のDocker」という意味で同等です)
準備1: ECRのリポジトリを作成する
以下のコマンドを実行して作成します。
(今回はテストであるためリポジトリポリシーやライフサイクルポリシーは設定しません)
$ aws ecr create-repository --repository-name go-webserver-arm64 --region ap-northeast-1
準備2: VPC環境および「Graviton2」インスタンスを構築する
CloudFormationのテンプレートを用意しました。
CloudFormationテンプレート (クリックすると展開します)
--- AWSTemplateFormatVersion: "2010-09-09" Description: "Launch 'Graviton2' EC2 instance with VPC environment" Metadata: AWS::CloudFormation::Interface: ParameterGroups: - Label: default: "General Information" Parameters: - SystemName - Label: default: "Network Configuration" Parameters: - CidrBlockVPC - CidrBlockSubnetPublic - MyIpAddressCidr - Label: default: "EC2 Instance Configuration" Parameters: - Graviton2ImageID - Graviton2InstanceType - Graviton2KeyName - Graviton2VolumeType - Graviton2VolumeSize Parameters: SystemName: Type: String Default: graviton2 CidrBlockVPC: Type: String Default: 192.168.0.0/16 CidrBlockSubnetPublic: Type: String Default: 192.168.1.0/24 MyIpAddressCidr: Type: String Graviton2ImageID: Type: AWS::SSM::Parameter::Value<String> Default: /aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-arm64-gp2 Graviton2InstanceType: Type: String Default: m6g.medium AllowedValues: - m6g.medium - m6g.large - m6g.xlarge - m6g.2xlarge - m6g.4xlarge - m6g.8xlarge - m6g.12xlarge - m6g.16xlarge Graviton2KeyName: Type: AWS::EC2::KeyPair::KeyName Graviton2VolumeType: Type: String Default: gp2 Graviton2VolumeSize: Type: String Default: 20 Resources: VPC: Type: AWS::EC2::VPC Properties: CidrBlock: !Ref CidrBlockVPC EnableDnsSupport: true EnableDnsHostnames: true InstanceTenancy: default Tags: - Key: Name Value: !Sub "${SystemName}-vpc" - Key: System Value: !Ref SystemName InternetGateway: Type: AWS::EC2::InternetGateway Properties: Tags: - Key: Name Value: !Sub "${SystemName}-igw" - Key: System Value: !Ref SystemName VPCGatewayAttachment: Type: AWS::EC2::VPCGatewayAttachment Properties: InternetGatewayId: !Ref InternetGateway VpcId: !Ref VPC SubnetPublic: Type: AWS::EC2::Subnet Properties: VpcId: !Ref VPC AvailabilityZone: !Select - 0 - Fn::GetAZs: !Ref AWS::Region CidrBlock: !Ref CidrBlockSubnetPublic MapPublicIpOnLaunch: true Tags: - Key: Name Value: !Sub "${SystemName}-public-subnet" - Key: System Value: !Ref SystemName RouteTablePublic: Type: AWS::EC2::RouteTable Properties: VpcId: !Ref VPC Tags: - Key: Name Value: !Sub "${SystemName}-public-rtb" - Key: System Value: !Ref SystemName RouteIGW: DependsOn: - VPCGatewayAttachment Type: AWS::EC2::Route Properties: RouteTableId: !Ref RouteTablePublic DestinationCidrBlock: 0.0.0.0/0 GatewayId: !Ref InternetGateway RouteTableAssociationPublic: Type: AWS::EC2::SubnetRouteTableAssociation Properties: SubnetId: !Ref SubnetPublic RouteTableId: !Ref RouteTablePublic SecurityGroupServer: Type: AWS::EC2::SecurityGroup Properties: GroupName: !Sub "${SystemName}-server-sg" GroupDescription: "Security group for server" VpcId: !Ref VPC SecurityGroupIngress: - IpProtocol: tcp FromPort: 22 ToPort: 22 CidrIp: !Ref MyIpAddressCidr - IpProtocol: tcp FromPort: 80 ToPort: 80 CidrIp: !Ref MyIpAddressCidr - IpProtocol: tcp FromPort: 8080 ToPort: 8080 CidrIp: !Ref MyIpAddressCidr Tags: - Key: Name Value: !Sub "${SystemName}-server-sg" - Key: System Value: !Ref SystemName IAMRoleServer: Type: AWS::IAM::Role Properties: RoleName: !Sub "${SystemName}-server-role" AssumeRolePolicyDocument: | { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Service": "ec2.amazonaws.com" }, "Action": "sts:AssumeRole" } ] } ManagedPolicyArns: - arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly Path: / IAMInstanceProfileServer: Type: AWS::IAM::InstanceProfile Properties: InstanceProfileName: !Sub "${SystemName}-server-role" Roles: - !Ref IAMRoleServer Path: / EC2InstanceGraviton2: Type: AWS::EC2::Instance Properties: ImageId: !Ref Graviton2ImageID InstanceType: !Ref Graviton2InstanceType KeyName: !Ref Graviton2KeyName BlockDeviceMappings: - DeviceName: /dev/xvda Ebs: VolumeType: !Ref Graviton2VolumeType VolumeSize: !Ref Graviton2VolumeSize NetworkInterfaces: - DeviceIndex: 0 SubnetId: !Ref SubnetPublic GroupSet: - !Ref SecurityGroupServer IamInstanceProfile: !Ref IAMInstanceProfileServer UserData: Fn::Base64: !Sub | #!/bin/bash -xe yum update -y # Install and Configure Docker amazon-linux-extras install -y docker=latest systemctl enable docker.service systemctl start docker.service usermod -aG docker ec2-user # Install and Configure Amazon ECR Credential Helper yum install -y amazon-ecr-credential-helper mkdir /home/ec2-user/.docker cat >> /home/ec2-user/.docker/config.json << EOF { "credsStore": "ecr-login" } EOF chown -R ec2-user:ec2-user /home/ec2-user/.docker/ Tags: - Key: Name Value: !Sub "${SystemName}-server" - Key: System Value: !Ref SystemName Outputs: VPC: Value: !Ref VPC Export: Name: !Sub "${AWS::StackName}::VPC" SubnetPublic: Value: !Ref SubnetPublic Export: Name: !Sub "${AWS::StackName}::SubnetPublic" SecurityGroupServer: Value: !Ref SecurityGroupServer Export: Name: !Sub "${AWS::StackName}::SecurityGroupServer" IAMRoleServer: Value: !Ref IAMRoleServer Export: Name: !Sub "${AWS::StackName}::IAMRoleServer" IAMInstanceProfileServer: Value: !Ref IAMInstanceProfileServer Export: Name: !Sub "${AWS::StackName}::IAMInstanceProfileServer" EC2InstanceGraviton2: Value: !Ref EC2InstanceGraviton2 Export: Name: !Sub "${AWS::StackName}::EC2InstanceGraviton2"
マネジメントコンソールで作成する場合は、以下をポイントにしてください。
- Dockerイメージを保存するために、ディスク容量を増やします。(8GB→20GB)
- 動作テストを行うために、セキュリティグループのインバウンドルールで「マイIP」からの「TCP/80」および「TCP/8080」の接続を許可します。
- ECRのリポジトリからイメージをプルできるよう、IAMポリシー「AmazonEC2ContainerRegistryReadOnly」を付与したIAMロールを設定します。
また、「Graviton2」インスタンスの起動後に、以下のインストール・設定を行います。
- Dockerのインストールと設定
- Amazon ECR Credential Helperのインストールと設定
Amazon ECR Credential Helper は、AWSのクレデンシャル情報を使ってECRへのログインを自動で行ってくれるユーティリティです。
(インストール・設定の手順については、CloudFormationテンプレートのEC2インスタンスUserDataの記述内容を参考にしてください)
準備3: Windows/Macの環境を準備する
手元のWindows PCまたはMacへ、以下の環境を導入してください。
WindowsやMacの環境が用意できない場合は、AWS上にAmazon Linux 2 (x86アーキテクチャ) のEC2インスタンスを起動して上記を導入した環境を使用しても構いません。
Step 1: Windows/Mac上でGo言語のビルドを行い、ARM向け実行可能ファイルを生成する
Go言語の特徴として「ビルドを実行する環境とは異なる環境向けの実行可能ファイルを生成することができる」という点があります。
ここで言う「環境」とは、OS (Linux、MaxOS、Windows、etc.) やCPUアーキテクチャ (x86、ARM、PowerPC、etc.) の組み合わせを指します。
今回の場合、OSは「Linux」、CPUアーキテクチャは「ARM64」を指定してビルドを行えば、「Graviton2」インスタンス上で実行可能なバイナリファイルを生成することができるという訳です。
それでは、実際に試してみましょう。
Go言語のビルドを行う
ソースコードを格納するディレクトリを作成します。
$ mkdir go-webserver-sample $ cd go-webserver-sample
以下の内容でソースコードを保存します。(内容は前回のブログ記事で用いたものと同じです)
package main import ( "fmt" "net/http" "os" "runtime" ) func handler(w http.ResponseWriter, r *http.Request) { hostname, _ := os.Hostname() fmt.Fprintf(w, "<h1>Welcome Golang-WebServer!</h1>") fmt.Fprintf(w, "<h2>Hostname: %s</h2>", hostname) fmt.Fprintf(w, "<h2>OS: %s</h2>", runtime.GOOS) fmt.Fprintf(w, "<h2>Architecture: %s</h2>", runtime.GOARCH) } func main() { http.HandleFunc("/", handler) http.ListenAndServe(":8080", nil) }
ソースコードをビルド (コンパイル) します。
OSやCPUアーキテクチャを指定してビルドを行うには、環境変数 (またはシェル変数) GOOS
およびGOARCH
を設定してからgo build
コマンドを実行します。
(併せて、前回のブログ記事で説明した「静的リンクによるビルド」を行うための環境変数およびオプションも指定する必要があります)
Windowsの場合:
> set CGO_ENABLED=0 > set GOOS=linux > set GOARCH=arm64 > go build -a -installsuffix cgo go-webserver-sample.go
MacまたはLinuxの場合:
$ CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -a -installsuffix cgo go-webserver-sample.go
ビルドが完了すると、ソースコードと同じディレクトリ上にgo-webser-sample
というファイルが生成されていると思います。
これが「ARM64アーキテクチャ向けの実行可能ファイル」なのですが、WindowsやMac上では確認する術が無いと思います。
(WindowsやMac上で実行しようとしても、エラーとなるか、そもそも実行可能ファイルとして認識されないはずです)
「Graviton2」インスタンス上で実行してみる
Windows/Mac上でビルドした「ARM64アーキテクチャ向け実行可能ファイル」を、SCP等を用いて「Graviton2」インスタンス上にコピーします。
実行可能ファイルが本当に「ARM64アーキテクチャ向け」なのかどうかを確認してみましょう。
$ file go-webserver-sample go-webserver-sample: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), statically linked, not stripped
「ARM aarch64」と出力されているので、間違いないようです。
それでは、起動してみましょう。
(ファイルパーミッションが設定されていない場合は設定します。例:chmod +x go-webserver-sample
)
$ ./go-webserver-sample
Webブラウザで「http://インスタンスのIPアドレス:8080」にアクセスすると、以下のような画面が表示されると思います。
Windows/MacでビルドしたGo言語の実行可能ファイルが、ARMアーキテクチャ上で動作することが分かりました。
Step 2: Windows/Mac上でDockerのビルドを行い、ARM向けDockerイメージを生成する
次はいよいよ「ARMアーキテクチャ向けDockerイメージ」のビルドを行いたいと思います。
Dockerのビルドには、前回のブログ記事で説明した「マルチステージビルド」を使います。
Dockerfileを記述する
Dockerfileを以下のように記述します。
FROM golang:latest AS builder WORKDIR /tmp COPY ./go-webserver-sample.go /tmp RUN CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -a -installsuffix cgo go-webserver-sample.go FROM alpine:latest@sha256:ad295e950e71627e9d0d14cdc533f4031d42edae31ab57a841c5b9588eacc280 COPY --from=builder /tmp/go-webserver-sample /bin/ CMD ["/bin/go-webserver-sample"]
内容は前回のブログ記事のDockerfileと似ていますが、2箇所だけ異なる部分があります。
1つ目は「ビルド用コンテナ」の4行目の記述です。
RUN CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -a -installsuffix cgo go-webserver-sample.go
「Step 1」で説明した通り、Windows/Mac上で「Linux」「ARM64アーキテクチャ」向けにGo言語のビルドを行うために、GOOS=linux
およびGOARCH=arm64
を指定しています。
2つ目は「実行用コンテナ」の1行目の記述です。
FROM alpine:latest@sha256:ad295e950e71627e9d0d14cdc533f4031d42edae31ab57a841c5b9588eacc280
通常のFROM
の記述に対して「@sha256:~」という文字列が追加されています。
「@sha256:」に続く64桁の16進数は、Dockerイメージの「ダイジェスト」と呼ばれます。
この行の記述は、Docker Hubの「マルチCPUアーキテクチャサポート」という仕組みを利用して、ARM64アーキテクチャ向けの「Alpine」をベースイメージとして用いることを意味しています。
「マルチCPUアーキテクチャサポート」とは
Docker Hubにおける「マルチCPUアーキテクチャサポート」とは、x86(AMD64)やARM64など複数のアーキテクチャ向けのイメージを同一のイメージ名・タグ名で管理することができる仕組みです。
Leverage multi-CPU architecture support | Docker Documentation
Docker Hubで公開されている公式イメージの多くは「マルチCPUアーキテクチャ」に対応しています。
公開されているイメージが「マルチCPUアーキテクチャ」に対応している場合、通常は、Docker Hubからプルを行うと自動的に適切なアーキテクチャのイメージがダウンロードされます。
しかし、各アーキテクチャのイメージで固有の「ダイジェスト」を明示することにより、要求元マシンのアーキテクチャに関係なく、指定したアーキテクチャ向けのイメージを利用することができるのです。
「ダイジェスト」を確認する手順は以下の通りです。
まず、Docker Hubの 「Alpine」イメージのページ を表示して、「Tags」タブを選択します。
「Alpine」イメージの各タグ毎に、用意されているOS/アーキテクチャが一覧表示されています。
ここで「linux/arm64/v8」をクリックします。
「linux/arm64/v8」向けイメージの詳細情報が表示されています。
このページの「DIGEST」に表示されているのが、目的のイメージの「ダイジェスト」です。
Dockerファイルの記述内容をおさらいしましょう。
- 「ビルド用コンテナ」
- 「x86アーキテクチャ」向けの「golang」イメージをベースイメージとして用います。
go build
のオプションを指定することにより、ARMアーキテクチャ向けの実行可能ファイルを生成します。- ビルド用コンテナは
docker build
を実行する環境でコンテナが起動されますので、x86アーキテクチャ向けのイメージでなければなりません。
- 「実行用コンテナ」
- 「ARMアーキテクチャ」向けの「Apline」イメージをベースイメージとして用います。(「ダイジェスト」を明示して指定)
- 「ビルド用コンテナ」で生成されたARMアーキテクチャ向けの実行可能ファイルを取り込みます。
- 実行用コンテナは
docker build
を実行する環境で起動される訳ではありませんので、ARMアーキテクチャ向けのイメージであっても問題ありません。
Dockerイメージをビルドする
Dockerのビルドを実行します。
$ docker image build -t go-webserver-arm64:latest . Sending build context to Docker daemon 7.17MB Step 1/7 : FROM golang:latest AS builder latest: Pulling from library/golang 376057ac6fa1: Pull complete 5a63a0a859d8: Pull complete 496548a8c952: Pull complete 2adae3950d4d: Pull complete 039b991354af: Pull complete 0cca3cbecb14: Pull complete 59c34b3f33f3: Pull complete Digest: sha256:b5114a530de5817bcc9b9b5f7b523b0424b75c78dd2f68d2b6d79dc858d98c9f Status: Downloaded newer image for golang:latest ---> 7e5e8028e8ec Step 2/7 : WORKDIR /tmp ---> Running in ea5dde95ba98 Removing intermediate container ea5dde95ba98 ---> d1f991163648 Step 3/7 : COPY ./go-webserver-sample.go /tmp ---> c58155f4bd9f Step 4/7 : RUN CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -a -installsuffix cgo go-webserver-sample.go ---> Running in 392724be71e9 Removing intermediate container 392724be71e9 ---> 69afad805908 Step 5/7 : FROM alpine:latest@sha256:ad295e950e71627e9d0d14cdc533f4031d42edae31ab57a841c5b9588eacc280 sha256:ad295e950e71627e9d0d14cdc533f4031d42edae31ab57a841c5b9588eacc280: Pulling from library/alpine 29e5d40040c1: Pull complete Digest: sha256:ad295e950e71627e9d0d14cdc533f4031d42edae31ab57a841c5b9588eacc280 Status: Downloaded newer image for alpine:latest@sha256:ad295e950e71627e9d0d14cdc533f4031d42edae31ab57a841c5b9588eacc280 ---> c20d2a9ab686 Step 6/7 : COPY --from=builder /tmp/go-webserver-sample /bin/ ---> 1aa89f0c856f Step 7/7 : CMD ["/bin/go-webserver-sample"] ---> Running in 93efffefee6e Removing intermediate container 93efffefee6e ---> e8f470825cb8 Successfully built e8f470825cb8 Successfully tagged go-webserver-arm64:latest
ビルドしたイメージを確認します。
$ docker image ls REPOSITORY TAG IMAGE ID CREATED SIZE go-webserver-arm64 latest e8f470825cb8 3 minutes ago 12.5MB <none> <none> 69afad805908 3 minutes ago 843MB golang latest 7e5e8028e8ec 2 days ago 810MB alpine <none> c20d2a9ab686 3 weeks ago 5.36MB
DockerイメージをECRリポジトリへプッシュする
まず、ECRレジストリへログインします。
ECRレジストリURLの123456789012
の部分は、ご自身の環境に合わせて変更してください。(以後も同様)
$ aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com Login Succeeded
ECRリポジトリの名前に合わせて、Dockerイメージにタグを追加します。
$ docker image tag go-webserver-arm64:latest 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/go-webserver-arm64:latest
DockerイメージをECRリポジトリへプッシュします。
$ docker image push 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/go-webserver-arm64:latest The push refers to repository [123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/go-webserver-arm64] b9df98922d0b: Pushed 678a0785e7d2: Pushed latest: digest: sha256:9378afc80cdbd9f239559ce10ee7ddb2e05be6fb3643a87db6600160bb358ccc size: 739
「Graviton2」インスタンスのDockerで、ECRリポジトリからイメージをプルして起動する
「Graviton2」インスタンス側を操作します。
ECRリポジトリからイメージをプルして起動します。
$ docker container run -p 80:8080 --rm 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/go-webserver-arm64:latest Unable to find image '123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/go-webserver-arm64:latest' locally latest: Pulling from go-webserver-arm64 29e5d40040c1: Pull complete 37649c7c5fba: Pull complete Digest: sha256:9378afc80cdbd9f239559ce10ee7ddb2e05be6fb3643a87db6600160bb358ccc Status: Downloaded newer image for 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/go-webserver-arm64:latest
Webブラウザで「Graviton2」インスタンスのIPアドレスにアクセスします。
Windows/Mac上でビルドした「ARMアーキテクチャ向け」Dockerイメージを使って、「Graviton2」上でコンテナを起動することができました。
おわりに
今回は、Go言語の「ビルドを実行する環境とは異なる環境向けの実行可能ファイルを生成することができる」という特徴を利用して、Windows/Mac上でARMアーキテクチャ向けのDockerイメージをビルドする方法をご紹介しました。
次回は、プログラム言語に依存しない方法として、Docker Desktopの「Buildx」拡張コマンドを利用したマルチCPUアーキテクチャイメージビルドを試してみたいと思います。